跳到主要内容

🙂浅学 Kotlin

现在在做的一个 Web 项目后端使用的 Kotlin 语言,它和 Java 一样都是编译成 JVM 字节码运行在 JVM 上。虽然它和 Java 有很多相似的地方,但在语言层面上增加了很多特性,特别是对函数式 (Functional) 编程范式的支持。在 Kotlin 中函数是一等公民,并且一切皆是表达式,因此也取消了代码中的分号。

本文是跟着 《Kotlin 官方文档 中文版》 学习,整理出和 Java 语法语义上不同的地方。

数据类型

数值

在 Kotlin 中,一切皆对象,在 Java 中的 8 种基本数据类型,在 Kotlin 中也是对象。例如下面这 4 种整数类型:

KotlinJava大小
Bytebyte1 byte
Shortshort2 byte
Intint4 byte
Longlong8 byte

Kotlin 在 JVM 平台中,数值存储的是 int, double 这种 Java 种的基本数值类型。但当左值为可空引用或者泛型时,数值会进行自动装箱,类比 Java 中的 Integer, Long 这种。

fun main() {
val a: Int = 10;
val b: Int? = 10; // 自动装箱
}

在 Kotlin 中,对 -128 ~ 127 的整数进行了内存优化,如果是可空引用,它引用的是相同的对象。

fun main() {
val a: Int? = 10;
val b: Int? = 10;

println(a === b) // 内存优化,引用相同的对象,所以打印 true

val c: Int? = 1000;
val d: Int? = 1000;

println(c === d) // false
}
== 和 === 比较数值

== 只比较左右值是否相等,是比较值的相等性;
=== 比较左右值是否是同一对象,是比较值的同一性。


Kotlin 在数值类型转换上也与 Java 有区别,Kotlin 的数值类型也是对象,而像 Int 和 Long 之间并没有实际关系,所以他们之间不能进行隐式转换,即便是将较小的类型赋值给较大的类型,这一点和 Java 是不同的。

fun main() {
val a: Int = 1000;
val b: Long = a; // 编译器报错
}

Kotlin 的数值类型提供了显示转换其它数值类型的方法,就像这样:

fun main() {
val a: Int = 1000;
val b: Long = a.toLong(); // 显示类型转换
}

字符串

Kotlin 中的原始字符串,类似多行字符串,它使用三个引号 """ 把字符串内容括起来。原始字符串中不能用转义字符,但是会保留换行和文本内容。

fun main() {
val text = """
for (c in "foo")
print(c)
"""

println(text)
}

使用 trimMargin() 函数移除原始字符串中的边界前缀,默认的边界前缀是 |

val text = """
|Tell me and I forget.
|Teach me and I remember.
|Involve me and I learn.
|(Benjamin Franklin)
""".trimMargin()

Kotlin 中也有和 JavaScript 中类似的字符串模板写法,在字符串字面值中使用 ${} 插入变量或者表达式的值,如果只插入一个变量,可以省略美元符后满的花括号。

fun main() {
val i = 10;
println("i = $i") // 字符串模板
}

数组

在 Kotlin 中使用 Array 类型表示数组,它内部提供了一个构造方法用来构造数组,以及 get,set 方法用来获取和修改数组元素(get set 编译后会转换为 [] 访问数组元素的形式)。

public class Array<T> {

public inline constructor(size: Int, init: (Int) -> T)

public operator fun get(index: Int): T

public operator fun set(index: Int, value: T): Unit

public val size: Int

public operator fun iterator(): Iterator<T>
}

一般有两种方式构建数组,第一种方式是使用 arrayOf() 函数,传入值来构建数组。

fun main() {
val arr = arrayOf(1, 2, 3, 4, 5) // 推断为 Array<Int> 类型
}

或者是使用构造函数,传入数组长度和一个函数参数,函数的实现用来构建数组中的元素。

fun main() {
val arr = Array(5) { i -> i * i } // 函数参数用来构造数组元素,i 是数组索引
arr.forEach { println(it) } // 0 1 4 9 16
}

上面这种泛型数组都是包装类型的数组,类比 Java 语言中的 Integer[] 或者 Object[] 这样。由于拆箱装箱会存在运行时开销,所以,Kotlin 也提供了原生类型数组。

Kotlin 中的原生数组类型:ByteArray, ShortArray, IntArray, LongArray 等,类比 Java 语言中的 byte[], int[] 这种。

fun main() {
val intArr = intArrayOf(1, 2, 3, 4, 5) // IntArray 类型
val intArr2 = IntArray(5) {i -> i}
}

可以存储任意类型的数组 Array<Any>,类比 Java 语言中的 Object[] 数组。

fun main() {
val arr: Array<Any> = arrayOf("one", 2, 3, 4, false)
}

需要注意的是,并不能将其它类型的数组赋值给 Array<Any>

fun main() {
val arrInt: Array<Int> = arrayOf(1,2,3,4,5)
val arrAny: Array<Any> = arrInt // 编译器报错
}

类型检测与类型转换

Kotlin 中提供的很多便捷的语义(语法糖),例如类型检测与自动类型转换时,Kotlin 中使用 is 或者 !is 操作符来判断变量是否是或者不是某种类型。当判断某个变量是某种类型时,如果为真,变量会在为真的条件上下文中自动进行类型转换。这样的操作在 Java 中需要显示地进行转换。

fun main() {
val s: Any = "Hello, world"

if (s is String) {
// 在为真条件上下文中,s 会隐式转换成 String 类型
println(s.length)
} else {
s.length // s仍然是 Any 类型,所以编译器报错
}
}

这种条件上下文不仅限于 if ,还有像 &&, || 这种条件运算,或者其它条件分支。

fun main() {
val s: Any = "Hello, world" // 相当于 Java 中的 Object 类型

// || 左侧表达式为 false 时才会执行右侧的表达式,在右侧上下文中,s 一定是 String 类型
if (s !is String || s.length > 0) {}

// && 左侧表达式为 true 时才会执行右侧的表达式,在右侧上下文中,s 一定是 String 类型
if (s is String && s.length > 0) {}
}

这种自动类型转换只能在特定的情况下完成,当编译器能保证变量在检测和使用过程中不可变时,才会完成自动转换。


“不安全的”类型转换操作符 as,使用 as 在无法完成类型转换的情况下会抛出异常,所以是”不安全的“。

fun main() {
val s: Any = "Hello, world"
val s1: String = s as String // 可以转换

val i: Any = 1
val i1: String = i as String // 不可转换,抛出异常
}

“安全的”类型转换操作符 as?,使用 as? 在无法完成类型转换的情况下不会抛出异常,所以叫做”安全的“类型转换操作。但会返回 null,这就要求左值必须是可空变量。

fun main() {
val i: Any = 1
val i1: String? = i as? String // 在无法转换的场景下会返回 null
println(i1) // null
}

流程控制

if 表达式

Kotlin 中的 if 和 Java 中的 if 用法类似,区别在于 Kotlin 中的 if 是一个表达式,它是可以出现在赋值符号右边的。如果 if 作为表达式使用赋值给变量时,满足条件的分支代码块中最后一个表达式会作为整个 if 表达式的返回值。如果没有返回值,默认返回单元类型(语义上表示空)。

fun main() {
val a = 10
val b = 20
val max = if (a > b) {
println("max value: a")
a
} else {
println("max value: b")
b
}
}

因为 if 是表达式,所以 Kotlin 中没有三元运算符。

when 表达式

Kotlin 中的 when 表达式和 Java 中的 switch 类似,但用法上更加灵活。

fun main() {
val a: Any = 10

when (a) {
1 -> println("a的值是1")
in 2..10 -> println("在某个范围")
!in 2..10 -> println("不在某个范围")
10, 11 -> println("a == 10 or a == 11")
is String -> {
val strLen:Int = a.length // 在此上下文中会自动类型转换
println("是 String 类型")
}
else -> println("")
}
}

for 循环

for 循环可以对任何提供了迭代器(实现 Iterable 接口)的类型进行遍历,类似 Java 中的增强for循环,都是使用迭代器遍历。

fun main() {
// 遍历 IntRange 类型
for (i in 1..10) {
println(i)
}

// 遍历 IntProgression 类型
for (i in 10 downTo 1 step 2) {
print("$i, ")
}

for (i in (10 downTo 1).reversed() step 2) {
print("$i, ")
}
}

使用 indices 遍历索引。

fun main() {
val langs = arrayOf("Java", "JavaScript", "Rust", "Kotlin")

for (index in langs.indices) {
println(langs[index])
}
}

使用 withIndex 在遍历时拿到索引和元素。

fun main() {
val langs = arrayOf("Java", "JavaScript", "Rust", "Kotlin")

// withIndex 遍历时会返回一个元组,元组中是当前遍历的索引和值
for ((index, value) in langs.withIndex()) {
println("$index : $value")
}
}

跳转与标签

在 Kotlin 的流程控制结构中,也有 break continue return 关键字,除了有和 Java 相同的用法外,还增加跳转或返回到指定标签的操作。

Kotlin 中的表达式前可以打标签,标签的写法是标识符后面跟着@,例如:loop@,abc@ 这样。当在循环表达式前打标签,在循环中就可以使用标签来限定 break continue return 的作用位置或者说作用范围。

例如使用标签限定 break, 在 break 后跟着标签的反写。当执行 break 时,break 语义会作用整个打标签的循环表达式。

fun main() {
foo()
}

fun foo() {
// for 循环表达式前打了一个标签 loop@
loop@ for (i in 1..10) {
println("i = $i")
for (j in 1..10) {
// 当 j == 3 时,会中止整个打标签的循环
if (j == 3) break@loop
println("j = $j")
}
}
}

如果 continue 后面跟着标签的反写,当执行到 continue 时,continue 语义会作用打标签的循环,继续下一次迭代。

fun main() {
foo()
}

fun foo() {
// for 循环表达式前打了一个标签 loop@
loop@ for (i in 1..10) {
println("i = $i")
for (j in 1..10) {
// 打标签的循环继续下一次迭代
if (j == 3) continue@loop
println("j = $j")
}
}
}

看一下下面这段代码,这段代码的执行结果如果按照 Java 的 forEach() 逻辑,结果应该打印除了 3 以外的 1 到 10 的数字,但实际只打印了 1 和 2。

fun main() {
foo()
}

fun foo() {
arrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).forEach {
if (it == 3) return
println(it)
}
}

在 Kotlin 中,只能对具名或匿名函数使用正常的、非限定的 return 来退出。 要退出一个 lambda 表达式,需要使用一个标签限定。

fun main() {
foo()
}

fun foo() {
arrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// 给 lambda 表达式打标签
.forEach aaa@{
// 标签限定return,只 return 打标签的lambda
if (it == 3) return@aaa
println(it)
}
}

如果 return 有返回值的话,返回值写在标签限定 return 后面。

异常处理

在 Kotlin 中, try...catch 也是表达式,catch 中可以返回值。

fun main() {
val result = try {
foo()
} catch (e: Exception) {
println("error msg: ${e.message}")
-1
}

println(result) // -1
}

fun foo(): Int {
return 1/0
}

在 Kotlin 中,所有的异常都是 Throwable 类的子类,并且没有 Java 中检查异常非检查异常之说。Java 中所有 RuntimeException 类及子类异常都是非检查异常,其余的都是检查异常。


Kotlin 中 throw 也是表达式,它返回 Nothing 类型。Nothing 类型没有值,它是用来标记永远不可能到达的位置。它还有一个可空变体,Nothing?

fun main() {
val result = try {
foo()
} catch (e: Exception) {
println("error msg: ${e.message}")
-1
}

println(result)
}

fun foo(): Nothing {
throw Exception()
}

类和对象

构造函数

Kotlin 中的构造函数和 Java 中的构造函数定义方式上有很大不同。Kotlin 中构造函数有两种,主构造函数和次构造函数

主构造函数定义在类声明位置,跟在类名后面,使用 constructor 关键字后面跟着小括号,小括号是主构造函数的参数列表。如果主构造函数没有访问修饰符,也没有使用注解的情况下,constructor 关键字可以省略不写。

class User constructor(
id: Long,
name: String,
age: Int
) {
// ...
}

主构造函数没有方法体,需要初始化的逻辑可以放到初始化块中。初始化块使用 init 关键字,后面跟着花括号。初始化块中的代码会在实例化对象时执行。通过反编译 Kotlin 的字节码看,init 块中的代码其实是放到了每个构造函数中的开始位置

在类中可以定义多个 init 块,这些初始化块编译后都会放到构造函数中执行,执行的顺序按 init 块声明的顺序从上往下执行。

class User constructor(
id: Long,
name: String,
age: Int
) {
// 初始化块
init {
// ...
}
}

在 Kotlin 类中还可以使用 constructor 关键字定义多个次构造函数。

class Animal {

lateinit var name: String

constructor(name: String) {
println("name: $name")
}

constructor() {
println("no parameters")
}

constructor(age: Int) {
println("age: $age")
}

init {
println("Animal init block.")
this.name = "花花"
}
}

需要注意的是,主构造函数和次构造函数都是可选的,如果类中存在主构造函数,那么次构造函数必须要直接或者间接委托主构造函数。

// 类声明中可以包含主构造函数
class User constructor(
id: Long,
name: String,
age: Int
) {

private var id: Long = id
private var name: String = name
private var age: Int = age
private lateinit var gender: String

// 次构造函数需要委托主构造函数
constructor(id: Long, name: String, age: Int, gender: String) : this(id, name, age) {
this.gender = gender
}

override fun toString(): String {
return "User[name: $name, age: $age]"
}

fun foo(v: Int = 10) {
println(gender)
}
}

实例化对象

在 Kotlin 中,实例化对象不需要使用 new 关键字。

fun main() {
// 实例化对象时,不需要 new 关键字
val user = User(1L, "花花", 3, "男")
println(user)
user.foo()
}

继承

整理 Kotlin 中独有的关键字

提示

Kotlin 中有很多关键字是 Java 中没有或者没有被使用的,这里是把这些关键字以及用法单独列出来。

constructor

constructor 关键字用来定义构造函数。如果是定义主构造函数,并且主构造函数没有访问修饰符和注解的情况下,主构造函数的 constructor 关键字可以省略。

init

init 关键字用于在类内定义初始化代码块,初始化代码块中的代码在对象实例化时执行。

使用 IDEA 中的反编译工具反编译 Kotlin 字节码,可以发现 init 代码块中的代码会被编译器放到构造函数中的开始位置。所以,初始化块中的代码会优于构造函数中的代码先执行,如果类中有多个 init 代码块,会按代码块顺序依次执行 init 代码块中的代码,然后再执行构造函数中的代码。

lateinit

lateinit 关键字顾名思义,稍后初始化的意思,用在成员变量前面,告诉编译器这个成员变量会稍后初始化。

在 Kotlin 中,成员位置定义的变量也需要初始化,否则编译器会报错。不是像 Java 那样有默认值,这么做应该是为了保证 Kotlin 的空安全机制。但即使变量声明时使用了 lateinit 关键字,也不是说这个变量不需要初始化,如果访问了没有初始化的变量,运行时会报错。

使用反编译工具发现,在访问变量时,会判断这个变量是否为 null,如果为 null 会抛异常。

源代码:

class Animal {

lateinit var name: String

fun foo() {
println(name)
}

}

反编译后的代码:

public final class Animal {
public String name;

@NotNull
public final String getName() {
String var10000 = this.name;
if (var10000 == null) {
Intrinsics.throwUninitializedPropertyAccessException("name");
}

return var10000;
}

public final void setName(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.name = var1;
}

public final void foo() {
String var10000 = this.name;
// 判断变量是否为 null
if (var10000 == null) {
Intrinsics.throwUninitializedPropertyAccessException("name");
}

String var1 = var10000;
boolean var2 = false;
System.out.println(var1);
}
}